{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 6.1 由来\n",
    "\n",
    "2010年3月我写了一篇[《学之者生，用之者死——ACE历史与简评》](../blog/chenshuo/5364096.md)，其中提到“我心目中理想的网络库”的样子\n",
    "- 线程安全，原生支持多核多线程。\n",
    "- 不考虑可移植性，不跨平台，只支持Linux，不支持Windows。\n",
    "- 主要支持x86-64，兼顾IA32。（实际上muduo也可以运行在ARM上。）\n",
    "- 不支持UDP，只支持TCP。\n",
    "- 不支持IPv6，只支持IPv4。\n",
    "- 不考虑广域网应用，只考虑局域网。（实际上muduo也可以用在广域网上。）\n",
    "- 不考虑公网，只考虑内网。不为安全性做特别的增强。\n",
    "- 只支持一种使用模式：非阻塞**IO+one event loop per thread**，不支持阻塞IO。\n",
    "- API简单易用，只暴露具体类和标准库里的类。API不使用non-trivial templates，也不使用虚函数。\n",
    "- 只满足常用需求的90%，不面面俱到，必要的时候以app来适应lib。\n",
    "- 只做library，不做成framework。\n",
    "- 争取全部代码在5000行以内（不含测试）。\n",
    "- 在不增加复杂度的前提下可以支持FreeBSD/Darwin，方便将来用Mac作为开发用机，但不为它做性能优化。也就是说，IO multiplexing使用`poll(2)`和`epoll(4)`。\n",
    "- 以上条件都满足时，可以考虑搭配Google Protocol Buffers RPC。\n",
    "\n",
    "\n",
    "在想清楚这些目标之后，我开始第三次尝试编写自己的C++网络库。与前两次不同，这次我一开始就想好了库的名字，叫[muduo(木铎)](https://kdocs.cn/l/cbzhL8vFkNYZ)，并在Google code上创建了项目：[http://code.google.com/p/muduo/](http://code.google.com/p/muduo/)。muduo以git为版本管理工具，托管于[https://github.com/chenshuo/muduo](https://github.com/chenshuo/muduo)。muduo的主体内容在2010年5月底已经基本完成，8月底发布0.1.0版，现在（2012年11月）的最新版。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 为什么需要网络库\n",
    "\n",
    "使用Sockets API进行网络编程是很容易上手的一项技术，花半天时间读完一两篇网上教程，相信不难写出能相互连通的网络程序。例如下面这个网络服务端和客户端程序，它用Python实现了一个简单的“Hello”协议，客户端发来姓名，服务端返回问候语和服务器的当前时间。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "python"
    }
   },
   "outputs": [],
   "source": [
    "#!/usr/bin/python\n",
    "\n",
    "# hello-server.py\n",
    "\n",
    "import socket\n",
    "import time\n",
    "\n",
    "serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "serversocket.bind(('', 8888))\n",
    "serversocket.listen(5)\n",
    "\n",
    "while True:\n",
    "    (clientsocket, address) = serversocket.accept()  # 等待客户端连接\n",
    "    data = clientsocket.recv(4096).decode()          # 接收姓名并解码\n",
    "    datetime = time.asctime()\n",
    "    response = f'Hello {data}\\nMy time is {datetime}\\n'\n",
    "    clientsocket.send(response.encode())             # 发送回复\n",
    "    clientsocket.close()                             # 关闭连接"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "python"
    }
   },
   "outputs": [],
   "source": [
    "#!/usr/bin/python\n",
    "\n",
    "# hello-client.py\n",
    "\n",
    "import socket\n",
    "import sys\n",
    "import getpass\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "sock.connect((sys.argv[1], 8888))                  # 服务器地址由命令行指定 (L22)\n",
    "username = getpass.getuser()                       # 获取当前用户名\n",
    "sock.send(username.encode())                       # 发送用户名到服务器\n",
    "message = sock.recv(4096)                          # 接收响应\n",
    "print(message.decode())                            # 打印结果\n",
    "sock.close()                                       # 关闭连接"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面两个程序使用了全部主要的Sockets API，包括`socket(2)、bind(2)、listen(2)、accept(2)、connect(2)、recv(2)、send(2)、close(2)、gethostbyname(3)`等，似乎网络编程一点也不难嘛。在同一台机器上运行上面的服务端和客户端，结果不出意料：\n",
    "> 代码中没有显式调用, 而是在L22隐式调用"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hello ningliu\n",
      "My time is Mon Aug 12 11:15:11 2024\n",
      "\n"
     ]
    }
   ],
   "source": [
    "! python3 hello-client.py 127.0.0.1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! python3 hello-client.py 127.0.0.1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是连接同一局域网的另外一台服务器时，收到的数据是不完整的。错在哪里？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! python3 hello-client.py 10.253.49.219 # 9500"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> 未实现书中描述的效果"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "出现这种情况的原因是高级语言（Java、Python等）的Sockets库并没有对Sockets API提供更高层的封装，直接用它编写网络程序很容易掉到陷阱里，因此我们需要一个好的网络库来降低开发难度。网络库的价值还在于能方便地处理并发连接(§ 6.6)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 6.2 安装"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[源码](https://github.com/chenshuo/muduo)\n",
    "- linux > 2.6.28: 使用了`timerfd`, `eventfd`\n",
    "- g++4.4\n",
    "- 非必需: curl, c-ares DNS, Google Protobuf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> ubuntu 22.04 下"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! sudo apt install libboost-all-dev libprotobuf-dev \\ \n",
    "     libcurl4-gnutls-dev libtcmalloc-minimal4 libc-ares-dev \\\n",
    "     libzzip-dev libhiredis-dev libgd-dev thrift-compiler protobuf-compiler"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**编译, 安装**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd ../muduo && ./build.sh -j32 # 编译muduo库和它自带的例子，生成的可执行文件和静态库文件 "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! ./build.sh install # 将 muduo 头文件和库文件安装到 ../build/debug-install/{include，lib}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! BUILD_TYPE=release ./build.sh "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! BUILD_TYPE=release ./build.sh install"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**运行**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd ../build/release-cpp11/bin && ./inspector_test"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "http://172.18.107.64:12345/\n",
    "\n",
    "```txt\n",
    "/proc/overview             print basic overview\n",
    "/proc/pid                  print pid\n",
    "/proc/status               print /proc/self/status\n",
    "/proc/threads              list /proc/self/task\n",
    "/sys/cpuinfo               print /proc/cpuinfo\n",
    "/sys/loadavg               print /proc/loadavg\n",
    "/sys/meminfo               print /proc/meminfo\n",
    "/sys/overview              print system overview\n",
    "/sys/stat                  print /proc/stat\n",
    "/sys/version               print /proc/version\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```shell\n",
    "$ ./inspector_test\n",
    "20240815 05:41:39.095449Z  8082 WARN  HttpServer[Inspector:test] starts listening on 0.0.0.0:12345 - HttpServer.cc:53\n",
    "20240815 05:42:44.311092Z  8082 INFO  TcpServer::newConnection [Inspector:test] - new connection [Inspector:test-0.0.0.0:12345#1] from 172.18.96.1:51948 - TcpServer.cc:80\n",
    "20240815 05:42:44.311220Z  8082 INFO  TcpServer::newConnection [Inspector:test] - new connection [Inspector:test-0.0.0.0:12345#2] from 172.18.96.1:51947 - TcpServer.cc:80\n",
    "20240815 05:43:44.680200Z  8082 INFO  TcpServer::removeConnectionInLoop [Inspector:test] - connection Inspector:test-0.0.0.0:12345#2 - TcpServer.cc:109\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**在自己的程序中使用 muduo**\n",
    "\n",
    "- https://github.com/chenshuo/muduo-tutorial"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 6.3 目录结构"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo 的目录结构如下\n",
    "\n",
    "```\n",
    "muduo\n",
    "|-- build.sh\n",
    "|-- ChangeLog\n",
    "|-- CMakeLists.txt\n",
    "|-- License\n",
    "|-- README\n",
    "|-- muduo                # muduo 库的主体\n",
    "|   |-- base             # 与网络无关的基础代码，位于 ::muduo namespace，包括线程库\n",
    "|   \\-- net              # 网络库，位于 ::muduo::net namespace\n",
    "|       |-- poller       # poll(2) 和 epoll(4) 两种 IO multiplexing 后端\n",
    "|       |-- http         # 一个简单的可嵌入的 Web 服务器\n",
    "|       |-- inspect      # 基于以上 Web 服务器的 \"窥探器\"，用于报告进程的状态\n",
    "|       \\-- protorpc     # 简单实现 Google Protobuf RPC，不推荐使用\n",
    "|-- examples             # 丰富的示例\n",
    "\\-- TODO\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo的源代码文件名与class名相同，例如`ThreadPool class`的定义是`muduo/base/ThreadPool.h`，其实现位于`muduo/base/ThreadPool.cc`。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 基础库"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`muduo/base`目录是一些基础库，都是用户可见的类，内容包括：\n",
    "\n",
    "```\n",
    "muduo\n",
    "\\-- base\n",
    "    |-- AsyncLogging.{h,cc}         # 异步日志 backend\n",
    "    |-- Atomic.h                    # 原子操作与原子整数\n",
    "    |-- BlockingQueue.h             # 无界阻塞队列（生产者消费者队列）\n",
    "    |-- BoundedBlockingQueue.h      # 有界阻塞队列\n",
    "    |-- Condition.h                 # 条件变量，与 Mutex.h 一同使用\n",
    "    |-- copyable.h                  # 一个空基类，用于标识（tag）值类型\n",
    "    |-- CountDownLatch.{h,cc}       # \"倒计时门闩\" 同步\n",
    "    |-- Date.{h,cc}                 # Julian 日期库（即公历）\n",
    "    |-- Exception.{h,cc}            # 带 stack trace 的异常基类\n",
    "    |-- Logging.{h,cc}              # 简单的日志，可搭配 AsyncLogging 使用\n",
    "    |-- Mutex.h                     # 互斥器\n",
    "    |-- ProcessInfo.{h,cc}          # 进程信息\n",
    "    |-- Singleton.h                 # 线程安全的 singleton\n",
    "    |-- StringPiece.h               # 从 Google 开源代码借用的字符串参数传递类型\n",
    "    |-- tests                       # 测试代码\n",
    "    |-- Thread.{h,cc}               # 线程对象\n",
    "    |-- ThreadLocal.h               # 线程局部数据\n",
    "    |-- ThreadLocalSingleton.h      # 每个线程一个 singleton\n",
    "    |-- ThreadPool.{h,cc}           # 简单的固定大小线程池\n",
    "    |-- Timestamp.{h,cc}            # UTC 时间戳\n",
    "    |-- TimeZone.{h,cc}             # 时区与夏令时\n",
    "    \\-- Types.h                     # 基本类型的声明，包括 muduo::string\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 网络核心库"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo是基于Reactor模式的网络库，其核心是个事件循环EventLoop，用于响应计时器和IO事件。muduo采用基于对象(object-based)而非面向对象(object-oriented)的设计风格，其事件回调接口多以`boost::function` + `boost::bind`表达，用户在使用muduo的时候不需要继承其中的class。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "网络库核心位于`muduo/net`和`muduo/net/poller`，一共不到4300行代码，以下灰底表示用户不可见的内部类。\n",
    "\n",
    "```\n",
    "muduo\n",
    "\\-- net\n",
    "    |-- Acceptor.{h,cc}             # 接受器，用于服务端接受连接(内部类)\n",
    "    |-- Buffer.{h,cc}               # 缓冲区，非阻塞 IO 必备\n",
    "    |-- Callbacks.h\n",
    "    |-- Channel.{h,cc}              # 用于每个 Socket 连接的事件分发\n",
    "    |-- CMakeLists.txt\n",
    "    |-- Connector.{h,cc}            # 连接器，用于客户端发起连接(内部类)\n",
    "    |-- Endian.h                    # 网络字节序与本机字节序的转换\n",
    "    |-- EventLoop.{h,cc}            # 事件分发器\n",
    "    |-- EventLoopThread.{h,cc}      # 新建一个专门用于 EventLoop 的线程\n",
    "    |-- EventLoopThreadPool.{h,cc}  # muduo 默认多线程 IO 模型\n",
    "    |-- InetAddress.{h,cc}          # IP 地址的简单封装\n",
    "    |-- Poller.{h,cc}               # IO multiplexing 的基类接口(内部类)\n",
    "    |-- poller                      # IO multiplexing 的实现(内部类)\n",
    "    |   |-- DefaultPoller.cc        # 根据环境变量 MUDUO_USE_POLL 选择后端\n",
    "    |   |-- EPollPoller.{h,cc}      # 基于 epoll(4) 的 IO multiplexing 后端\n",
    "    |   \\-- PollPoller.{h,cc}       # 基于 poll(2) 的 IO multiplexing 后端\n",
    "    |-- Socket.{h,cc}               # 封装 Sockets 描述符，负责关闭连接(内部类)\n",
    "    |-- SocketsOps.{h,cc}           # 封装底层的 Sockets API(内部类)\n",
    "    |-- TcpClient.{h,cc}            # TCP 客户端\n",
    "    |-- TcpConnection.{h,cc}        # muduo 里最大的一个类，有 300 多行\n",
    "    |-- TcpServer.{h,cc}            # TCP 服务端\n",
    "    |-- tests                       # 简单测试\n",
    "    |-- Timer.{h,cc}                # 以下几个文件与定时器回调相关(内部类)\n",
    "    |-- TimerId.h\n",
    "    \\-- TimerQueue.{h,cc}           # (内部类)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 网络附属库"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "网络库有一些附属模块，它们不是核心内容，在使用的时候需要链接相应的库，例如`-lmuduo_http`、`-lmuduo_inspect`等等。`HttpServer`和`Inspector`暴露出一个http界面，用于监控进程的状态，类似于Java JMX(§ 9.5)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "附属模块位于`muduo/net/{http，inspect，protorpc}`等处。\n",
    "\n",
    "```\n",
    "muduo\n",
    "\\-- net\n",
    "    |-- http    # 不打算做成通用的 HTTP 服务器，这只是简陋而不完整的 HTTP 协议实现\n",
    "    |   |-- CMakeLists.txt\n",
    "    |   |-- HttpContext.h\n",
    "    |   |-- HttpRequest.h\n",
    "    |   |-- HttpResponse.{h,cc}\n",
    "    |   |-- HttpServer.{h,cc}\n",
    "    |   \\-- tests/HttpServer_test.cc  # 示范如何在程序中嵌入 HTTP 服务器\n",
    "    |-- inspect  # 基于 HTTP 协议的窥探器，用于报告进程的状态\n",
    "    |   |-- CMakeLists.txt\n",
    "    |   |-- Inspector.{h,cc}\n",
    "    |   |-- ProcessInspector.{h,cc}\n",
    "    |   \\-- tests/Inspector_test.cc   # 示范暴露程序状态，包括内存使用和文件描述符\n",
    "    \\-- protorpc  # 简单实现 Google Protobuf RPC\n",
    "        |-- CMakeLists.txt\n",
    "        |-- google-inl.h\n",
    "        |-- RpcChannel.{h,cc}\n",
    "        |-- RpcCodec.{h,cc}\n",
    "        |-- rpc.proto\n",
    "        \\-- RpcServer.{h,cc}\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.3.1 代码结构"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo的头文件明确分为客户可见和客户不可见两类。以下是安装之后暴露的头文件和库文件。对于使用muduo库而言，只需要掌握5个关键类：`Buffer`、`EventLoop`、`TcpConnection`、`TcpClient`、`TcpServer`。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "|-- include                        # 头文件\n",
    "|   \\-- muduo\n",
    "|       |-- base                   # 基础库，同前，略\n",
    "|       \\-- net                    # 网络核心库\n",
    "|           |-- Buffer.h\n",
    "|           |-- Callbacks.h\n",
    "|           |-- Channel.h\n",
    "|           |-- Endian.h\n",
    "|           |-- EventLoop.h\n",
    "|           |-- EventLoopThread.h\n",
    "|           |-- InetAddress.h\n",
    "|           |-- TcpClient.h\n",
    "|           |-- TcpConnection.h\n",
    "|           |-- TcpServer.h\n",
    "|           |-- TimerId.h\n",
    "|           |-- http               # 以下为网络附属库的头文件\n",
    "|           |   |-- HttpRequest.h\n",
    "|           |   |-- HttpResponse.h\n",
    "|           |   \\-- HttpServer.h\n",
    "|           |-- inspect\n",
    "|           |   |-- Inspector.h\n",
    "|           |   \\-- ProcessInspector.h\n",
    "|           \\-- protorpc\n",
    "|               |-- RpcChannel.h\n",
    "|               |-- RpcCodec.h\n",
    "|               \\-- RpcServer.h\n",
    "\\-- lib                            # 静态库文件\n",
    "    |-- libmuduo_base.a, libmuduo_net.a\n",
    "    |-- libmuduo_http.a, libmuduo_inspect.a\n",
    "    \\-- libmuduo_protorpc.a\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- muduo的网络核心库的头文件包含关系，用户可见的为白底，用户不可见的为灰底\n",
    "\n",
    "\n",
    "<img src=\"./images/6.1.jpg\" alt=\"6.1\" style=\"zoom:30%;\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo头文件中使用了**前向声明**(forward declaration)，大大简化了头文件之间的依赖关系。例如`Acceptor.h、Channel.h、Connector.h、TcpConnection.h`都前向声明了`EventLoop class`，从而避免包含`EventLoop.h`。另外，`TcpClient.h`前向声明了`Connector class`，从而避免将内部类暴露给用户，类似的做法还有`TcpServer.h`用到的`Acceptor`和`EventLoopThreadPool`、`EventLoop.h`用到的`Poller`和`TimerQueue`、`TcpConnection.h`用到的`Channel`和`Socket`等等。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里简单介绍各个class的作用，详细的介绍参见后文。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 公开接口"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- `Buffer`仿Netty ChannelBuffer的`buffer class`，数据的读写通过`buffer`进行。用户代码不需要调用`read(2)/write(2)`，只需要处理收到的数据和准备好要发送的数据(§7.4)。\n",
    "- `InetAddress`封装IPv4地址(end point)，注意，它不能解析域名，只认IP地址。因为直接用`gethostbyname(3)`解析域名会阻塞IO线程。\n",
    "- `EventLoop`事件循环（反应器Reactor）​，每个线程只能有一个`EventLoop`实体，它负责IO和定时器事件的分派。它用`eventfd(2)`来异步唤醒，这有别于传统的用一对`pipe(2)`的办法。它用`TimerQueue`作为计时器管理，用`Poller`作为IO multiplexing。\n",
    "- `EventLoopThread`启动一个线程，在其中运行`EventLoop::loop()`。\n",
    "- `TcpConnection`整个网络库的核心，封装一次TCP连接，注意它不能发起连接。\n",
    "- `TcpClient`用于编写网络客户端，能发起连接，并且有重试功能\n",
    "- `TcpServer`用于编写网络服务器，接受客户的连接\n",
    "\n",
    "在这些类中，`TcpConnection`的生命期依靠`shared_ptr`管理（即用户和库共同控制）​。`Buffer`的生命期由`TcpConnection`控制。其余类的生命期由用户控制。`Buffer`和`InetAddress`具有值语义，可以拷贝；其他class都是对象语义，不可以拷贝。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 内部实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- `Channel`是selectable IO channel，负责注册与响应IO事件，注意它不拥有`file descriptor`。它是`Acceptor、Connector、EventLoop、TimerQueue、TcpConnection`的成员，生命期由后者控制。\n",
    "- `Socket`是一个RAII handle，封装一个`file descriptor`，并在析构时关闭`fd`。它是`Acceptor、TcpConnection`的成员，生命期由后者控制。`EventLoop、TimerQueue`也拥有`fd`，但是不封装为Socket class。\n",
    "- `SocketsOps`封装各种Sockets系统调用\n",
    "- `Poller`是`PollPoller`和`EPollPoller`的基类，采用“电平触发”的语意。它是`EventLoop`的成员，生命期由后者控制。\n",
    "- `Connector`用于发起TCP连接，它是`TcpClient`的成员，生命期由后者控制。\n",
    "- `Acceptor`用于接受TCP连接，它是`TcpServer`的成员，生命期由后者控制。\n",
    "- `TimerQueue`用`timerfd`实现定时，这有别于传统的设置`poll/epoll_wait`的等待时长的办法。`TimerQueue`用`std::map`来管理`Timer`，常用操作的复杂度是O(log N)，N为定时器数目。它是`EventLoop`的成员，生命期由后者控制。\n",
    "- `EventLoopThreadPool`用于创建IO线程池，用于把`TcpConnection`分派到某个`EventLoop`线程上。它是`TcpServer`的成员，生命期由后者控制。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- muduo的简化类图，`Buffer`是`TcpConnection`的成员\n",
    "\n",
    "<img src=\"./images/6.2.jpg\" alt=\"6.2\" style=\"zoom:30%;\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.3.2 例子"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo附带了十几个示例程序，编译出来有近百个可执行文件。这些例子位于examples目录，其中包括从Boost.Asio、Java Netty、Python Twisted等处移植过来的例子。这些例子基本覆盖了常见的服务端网络编程功能点，从这些例子可以充分学习非阻塞网络编程。\n",
    "\n",
    "```\n",
    "examples\n",
    "|-- asio                  # 从 Boost.Asio 移植的例子\n",
    "|   |-- chat              # 多人聊天的服务端和客户端，示范打包和拆包（codec）\n",
    "|   \\-- tutorial          # 一系列 timers\n",
    "|-- cdns                  # 基于 c-ares 的异步 DNS 解析\n",
    "|-- curl                  # 基于 curl 的异步 HTTP 客户端\n",
    "|-- filetransfer          # 简单的文件传输，示范完整发送 TCP 数据\n",
    "|-- hub                   # 一个简单的 pub/sub/hub 服务，演示应用级的广播\n",
    "|-- idleconnection        # 踢掉空闲连接\n",
    "|-- maxconnection         # 控制最大连接数\n",
    "|-- multiplexer           # 1:n 串并转换服务\n",
    "|-- netty                 # 从 JBoss Netty 移植的例子\n",
    "|   |-- discard           # 可用于测试带宽，服务器可多线程运行\n",
    "|   |-- echo              # 可用于测试带宽，服务器可多线程运行\n",
    "|   \\-- uptime            # 带自动重连的 TCP 长连接客户端\n",
    "|-- pingpong              # pingpong 协议，用于测试消息吞吐量\n",
    "|-- protobuf              # Google Protobuf 的网络传输示例\n",
    "|   |-- codec             # 自动反射消息类型的传输方案\n",
    "|   |-- rpc               # RPC 示例，实现 Sudoku 服务\n",
    "|   \\-- rpcbench          # RPC 性能测试示例\n",
    "|-- roundtrip             # 测试两台机器的网络延时与时间差\n",
    "|-- shorturl              # 简单的短址服务\n",
    "|-- simple                # 5 个简单网络协议的实现\n",
    "|   |-- allinone          # 在一个程序里同时实现下面 5 个协议\n",
    "|   |-- chargen           # RFC 864，可测试带宽\n",
    "|   |-- chargenclient     # chargen 的客户端\n",
    "|   |-- daytime           # RFC 867\n",
    "|   |-- discard           # RFC 863\n",
    "|   |-- echo              # RFC 862\n",
    "|   |-- time              # RFC 868\n",
    "|   \\-- timeclient        # time 协议的客户端\n",
    "|-- socks4a               # Socks4a 代理服务器，示范动态创建 TcpClient\n",
    "|-- sudoku                # 数独求解器，示范 muduo 的多线程模型\n",
    "|-- twisted               # 从 Python Twisted 移植的例子\n",
    "|   \\-- finger            # finger01 ~ 07\n",
    "\\-- zeromq                # 从 ZeroMQ 移植的性能（消息延迟）测试\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另外还有几个基于muduo的示例项目，由于License等原因没有放到muduo发行版中，可以单独下载。\n",
    "- http://github.com/chenshuo/muduo-udns：基于UDNS的异步DNS解析\n",
    "- http://github.com/chenshuo/muduo-protorpc：新的RPC实现，自动管理对象生命期\n",
    "\n",
    "> 注意，目前 muduo-protorpc与Ubuntu Linux12.04中通过apt-get安装的Protobuf编译器无法配合，请从源码编译安装Protobuf 2.4.1。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.3.3 线程模型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo的线程模型符合我主张的**one loop per thread + thread pool**模型。每个线程最多有一个`EventLoop`，每个`TcpConnection`必须归某个`EventLoop`管理，所有的IO会转移到这个线程。换句话说，一个`file descriptor`只能由一个线程读写。`TcpConnection`所在的线程由其所属的`EventLoop`决定，这样我们可以很方便地把不同的TCP连接放到不同的线程去，也可以把一些TCP连接放到一个线程里。`TcpConnection`和`EventLoop`是线程安全的，可以跨线程调用。\n",
    "\n",
    "> AI: 消息传递模型：跨线程的操作通过消息传递（实际上是函数调用）来完成，而不是直接访问共享状态。这避免了需要使用互斥锁来保护共享数据的情况。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`TcpServer`直接支持多线程，它有两种模式：\n",
    "- 单线程，`accept(2)`与`TcpConnection`用同一个线程做IO。\n",
    "- 多线程，accept(2)与EventLoop在同一个线程，另外创建一个EventLoop-ThreadPool，新到的连接会按round-robin方式分配到线程池中。\n",
    "\n",
    "后文§ 6.6还会以Sudoku服务器为例再次介绍muduo的多线程模型。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 结语"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo是我对常见网络编程任务的总结，用它我能很容易地编写多线程的TCP服务器和客户端。muduo是我业余时间的作品，代码估计还有一些bug，功能也不完善（例如不支持`signal`处理）​，待日后慢慢改进吧。\n",
    "> Signal 也可以通过 `signalfd(2)` 融入 `EventLoop` 中，见 `muduo-protorpc` 中的 `zurg slave` 例子"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 6.4 使用教程"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本节主要介绍muduo网络库的使用，其设计与实现将在第8章讲解。\n",
    "\n",
    "muduo只支持Linux 2.6.x下的并发非阻塞TCP网络编程，它的核心是每个IO线程一个事件循环，把IO事件分发到回调函数上。\n",
    "\n",
    "我编写muduo网络库的目的之一就是简化日常的TCP网络编程，让程序员能把精力集中在业务逻辑的实现上，而不要天天和Sockets API较劲。借用Brooks的话说，我希望muduo能减少网络编程中的偶发复杂性(accidental complexity)。\n",
    "> http://www.cs.nott.ac.uk/~pszcah/G51ISS/Documents/NoSilverBullet.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.4.1 TCP网络编程本质论"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "基于事件的非阻塞网络编程是编写高性能并发网络服务程序的主流模式，头一次使用这种方式编程通常需要**转换思维模式**。\n",
    "- 把原来“主动调用`recv(2)`来接收数据，主动调用`accept(2)`来接受新连接，主动调用`send(2)`来发送数据”的思路\n",
    "- 换成“注册一个收数据的回调，网络库收到数据会调用我，直接把数据提供给我，供我消费。注册一个接受连接的回调，网络库接受了新连接会回调我，直接把新的连接对象传给我，供我使用。需要发送数据的时候，只管往连接中写，网络库会负责无阻塞地发送。”\n",
    "\n",
    "这种编程方式有点像Win32的消息循环，消息循环中的代码应该避免阻塞，否则会让整个窗口失去响应，同理，事件处理函数也应该避免阻塞，否则会让网络服务失去响应。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我认为，TCP网络编程最本质的是处理**三个半事件**：\n",
    "\n",
    "1. 连接的建立，包括服务端接受(`accept`)新连接和客户端成功发起(`connect`)连接。TCP连接一旦建立，客户端和服务端是平等的，可以各自收发数据。\n",
    "\n",
    "2. 连接的断开，包括主动断开(`close`、`shutdown`)和被动断开（`read(2)`)返回`0`\n",
    "\n",
    "3. 消息到达，文件描述符可读。这是最为重要的一个事件，对它的处理方式决定了网络编程的风格（阻塞还是非阻塞，如何处理分包，应用层的缓冲如何设计，等等）​。<br/>\n",
    "\n",
    "4. 3.5 消息发送完毕，这算半个。对于低流量的服务，可以不必关心这个事件；另外，这里的“发送完毕”是指将数据写入操作系统的缓冲区，将由TCP协议栈负责数据的发送与重传，不代表对方已经收到数据。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这其中有很多难点，也有很多细节需要注意，比方说：\n",
    "\n",
    "- 如果要主动关闭连接，如何保证对方已经收到全部数据？如果应用层有缓冲（这在非阻塞网络编程中是必需的，见下文）​，那么如何保证先发送完缓冲区中的数据，然后再断开连接？直接调用`close(2)`恐怕是不行的。\n",
    "\n",
    "- 如果主动发起连接，但是对方主动拒绝，如何定期（带back-off地）重试？\n",
    "\n",
    "- 非阻塞网络编程该用边沿触发(edge trigger)还是电平触发(level trigger)？如果是电平触发，那么什么时候关注`EPOLLOUT`事件？会不会造成busy-loop？如果是边沿触发，如何防止漏读造成的饥饿？`epoll(4)`一定比`poll(2)`快吗？\n",
    "> 这两个中文术语有其他译法，我选择了一个电子工程师熟悉的说法。边缘触发/水平触发\n",
    "\n",
    "- 在非阻塞网络编程中，为什么要使用应用层发送缓冲区？假设应用程序需要发送40kB数据，但是操作系统的TCP发送缓冲区只有25kB剩余空间，那么剩下的15kB数据怎么办？如果等待OS缓冲区可用，会阻塞当前线程，因为不知道对方什么时候收到并读取数据。因此网络库应该把这15kB数据缓存起来，放到这个TCP链接的应用层发送缓冲区中，等socket变得可写的时候立刻发送数据，这样“发送”操作不会阻塞。如果应用程序随后又要发送50kB数据，而此时发送缓冲区中尚有未发送的数据（若干kB）​，那么网络库应该将这50kB数据追加到发送缓冲区的末尾，而不能立刻尝试`write()`，因为这样有可能打乱数据的顺序。\n",
    "\n",
    "- 在非阻塞网络编程中，为什么要使用应用层接收缓冲区？假如一次读到的数据不够一个完整的数据包，那么这些已经读到的数据是不是应该先暂存在某个地方，等剩余的数据收到之后再一并处理？见lighttpd关于`\\r\\n\\r\\n`分包的bug。假如数据是一个字节一个字节地到达，间隔10ms，每个字节触发一次文件描述符可读(readable)事件，程序是否还能正常工作？lighttpd在这个问题上出过安全漏洞。\n",
    "> http://redmine.lighttpd.net/issues/show/2105\n",
    "> http://download.lighttpd.net/lighttpd/security/lighttpd_sa_2010_01.txt\n",
    "\n",
    "- 在非阻塞网络编程中，如何设计并使用缓冲区？一方面我们希望减少系统调用，一次读的数据越多越划算，那么似乎应该准备一个大的缓冲区。另一方面，我们希望减少内存占用。如果有10000个并发连接，每个连接一建立就分配各50kB的读写缓冲区(s)的话，将占用1GB内存，而大多数时候这些缓冲区的使用率很低。muduo用`readv(2)`结合栈上空间巧妙地解决了这个问题。\n",
    "\n",
    "- 如果使用发送缓冲区，万一接收方处理缓慢，数据会不会一直堆积在发送方，造成内存暴涨？如何做应用层的流量控制？\n",
    "\n",
    "- 如何设计并实现定时器？并使之与网络IO共用一个线程，以避免锁。\n",
    "\n",
    "这些问题在muduo的代码中可以找到答案。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.4.2 echo服务的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "muduo的使用非常简单，不需要从指定的类派生，也不用覆写虚函数，只需要注册几个回调函数去处理前面提到的三个半事件就行了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面以经典的echo回显服务为例：\n",
    "\n",
    "> https://github.com/chenshuo/muduo/tree/master/examples/simple/echo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 定义`EchoServer class`，不需要派生自任何基类, [echo.h](./echo/echo.h)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 在构造函数里注册回调函数, [echo.cc](./echo/echo.cc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 实现`EchoServer::onConnection()`和`EchoServer::onMessage()`, [echo.cc](./echo/echo.cc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在`onConnection()`函数中，`conn`参数是`TcpConnection`对象的`shared_ptr`，`TcpConnection::connected()`返回一个`bool`值，表明目前连接是建立还是断开，`TcpConnection`的`peerAddress()`和`localAddress()`成员函数分别返回对方和本地的地址（以`InetAddress`对象表示的`IP`和`port`）​。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在`onMessage()`函数中，\n",
    "- `conn`参数是收到数据的那个TCP连接；\n",
    "- `buf`是已经收到的数据，buf的数据会累积，直到用户从中取走(retrieve)数据。注意`buf`是指针，表明用户代码可以修改（消费）`buffer`；\n",
    "- `time`是收到数据的确切时间，即`epoll_wait(2)`返回的时间，注意这个时间通常比`read(2)`发生的时间略早，可以用于正确测量程序的消息处理延迟。另外，`Timestamp`对象采用pass-by-value，而不是pass-by-(const)reference，这是有意的，因为在x86-64上可以直接通过寄存器传参。\n",
    "\n",
    "> AI: 在x86-64架构上，前几个函数参数通常通过寄存器传递，而不是栈。这对于小型对象(通常不超过2-3个字长)来说特别高效"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L37和L40是echo服务的“业务逻辑”​：把收到的数据原封不动地发回客户端。注意我们不用担心L40的`send(msg)`是否完整地发送了数据，因为muduo网络库会帮我们管理发送缓冲区。\n",
    "\n",
    "这两个函数体现了“**基于事件编程**”的典型做法，即程序主体是被动等待事件发生，事件发生之后网络库会调用（回调）事先注册的事件处理函数(event handler)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. 在`main()`里用`EventLoop`让整个程序跑起来。[main.cc](./echo/main.cc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个几十行的小程序实现了一个单线程并发的echo服务程序，可以同时处理多个连接。\n",
    "\n",
    "这个程序用到了`TcpServer`、`EventLoop`、`TcpConnection`、`Buffer`这几个class，也大致反映了这几个class的典型用法，后文还会详细介绍这几个class。注意，以后的代码大多会省略namespace。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd ../ && rm -rf build && cmake -S . -B build && cmake --build build --target simple_echo --config Release "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hello, World!\n"
     ]
    }
   ],
   "source": [
    "! echo \"Hello, World!\" | nc 127.0.0.1 2007"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd ../run && ./simple_echo\n",
    "20241008 08:45:17.126605Z 211265 INFO  pid = 211265 - main.cc:13\n",
    "20241008 08:45:19.263203Z 211265 INFO  TcpServer::newConnection [EchoServer] - new connection [EchoServer-0.0.0.0:2007#1] from 127.0.0.1:52846 - TcpServer.cc:80\n",
    "20241008 08:45:19.263288Z 211265 INFO  EchoServer - 127.0.0.1:52846 -> 127.0.0.1:2007 is UP - echo.cc:29\n",
    "20241008 08:45:19.263349Z 211265 INFO  EchoServer-0.0.0.0:2007#1 echo 14 bytes, data received at 1728377119.263304 - echo.cc:39"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.4.3 七步实现finger服务"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Python Twisted是一款非常好的网络库，它也采用Reactor作为网络编程的基本模型，所以从使用上与muduo颇有相似之处（当然，muduo没有deferreds）​。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "finger是Twisted文档的一个经典例子，本文展示如何用muduo来实现最简单的finger服务端。限于篇幅，只实现finger01～finger07。代码位于[examples/twisted/finger](https://github.com/chenshuo/muduo/tree/master/examples/twisted/finger)。\n",
    "> AI: Finger服务器是一种较老的网络服务,主要用于提供关于系统用户的信息。它在互联网早期比较流行,但现在已经不太常见了"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 拒绝连接。什么都不做，程序空等。[finger01.cc](./finger/finger01.cc)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "telnet: Unable to connect to remote host: Connection refused\n",
      "Trying 127.0.0.1...\n"
     ]
    }
   ],
   "source": [
    "! telnet 127.0.0.1 1079"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 接受新连接。 在1079端口侦听新连接，接受连接之后什么都不做，程序空等。muduo会自动丢弃收到的数据。[finger02.cc](./finger/finger02.cc)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Trying 127.0.0.1...\n",
      "Connected to 127.0.0.1.\n",
      "Escape character is '^]'.\n",
      "Connection closed by foreign host.\n"
     ]
    }
   ],
   "source": [
    "! telnet 127.0.0.1 1079"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "$ ./twisted_finger02\n",
    "20241009 01:45:39.374683Z 479677 INFO  TcpServer::newConnection [Finger] - new connection [Finger-0.0.0.0:1079#1] from 127.0.0.1:41988 - TcpServer.cc:80"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. 主动断开连接。 接受新连接之后主动断开。以下省略头文件和namespace。[finger03.cc](./finger/finger03.cc)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Trying 127.0.0.1...\n",
      "Connected to 127.0.0.1.\n",
      "Escape character is '^]'.\n",
      "Connection closed by foreign host.\n"
     ]
    }
   ],
   "source": [
    "! telnet 127.0.0.1 1079"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! ./twisted_finger03\n",
    "20241009 01:48:30.967279Z 480402 INFO  TcpServer::newConnection [Finger] - new connection [Finger-0.0.0.0:1079#1] from 127.0.0.1:52866 - TcpServer.cc:80\n",
    "20241009 01:48:30.967895Z 480402 INFO  TcpServer::removeConnectionInLoop [Finger] - connection Finger-0.0.0.0:1079#1 - TcpServer.cc:109"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4. 读取用户名，然后断开连接。 如果读到一行以`\\r\\n`结尾的消息，就断开连接。注意这段代码有安全问题，如果恶意客户端不断发送数据而不换行，会撑爆服务端的内存。另外，`Buffer::findCRLF()`是线性查找，如果客户端每次发一个字节，服务端的时间复杂度为$O(N^2)$，会消耗CPU资源。[finger04.cc](./finger/finger04.cc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5. 读取用户名、输出错误信息，然后断开连接。 如果读到一行以`\\r\\n`结尾的消息，就发送一条出错信息，然后断开连接。安全问题同上。[finger05.cc](./finger/finger05.cc)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Trying 127.0.0.1...\n",
      "Connected to 127.0.0.1.\n",
      "Escape character is '^]'.\n",
      "Connection closed by foreign host.\n"
     ]
    }
   ],
   "source": [
    "! telnet 127.0.0.1 1079"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! ./twisted_finger05\n",
    "20241009 02:11:41.194216Z 484754 INFO  TcpServer::newConnection [Finger] - new connection [Finger-0.0.0.0:1079#1] from 127.0.0.1:54326 - TcpServer.cc:80"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "6. 从空的`UserMap`里查找用户。 从一行消息中拿到用户名(L30)，在`UserMap`里查找，然后返回结果。安全问题同上。[finger06.cc](./finger/finger06.cc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7. 往`UserMap`里添加一个用户。 与前面几乎完全一样，只多了L39. [finger07.cc](./finger/finger07.cc)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! telnet 127.0.0.1 1079\n",
    "Trying 127.0.0.1...\n",
    "Connected to 127.0.0.1.\n",
    "Escape character is '^]'.\n",
    "muduo\n",
    "No such user\n",
    "Connection closed by foreign host."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! telnet 127.0.0.1 1079\n",
    "Trying 127.0.0.1...\n",
    "Connected to 127.0.0.1.\n",
    "Escape character is '^]'.\n",
    "schen\n",
    "Happy and well\n",
    "Connection closed by foreign host."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 6.5 性能评测"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我在一开始编写muduo的时候并没有以高性能为首要目标。在2010年8月发布之后，有网友询问其性能与其他常见网络库相比如何，因此我才加入了一些性能对比的示例代码。我很惊奇地发现，在muduo擅长的领域（TCP长连接），其性能不比任何开源网络库差。\n",
    "\n",
    "性能对比原则：采用对方的性能测试方案，用muduo实现功能相同或类似的程序，然后放到相同的软硬件环境中对比。\n",
    "\n",
    "注意这里的测试只是简单地比较了平均值；其实在严肃的性能对比中至少还应该考虑分布和百分位数(percentile)的值。限于篇幅，此处从略。\n",
    ">- 失效 http://zedshaw.com/essays/programmer_stats.html\n",
    ">- 失效 http://www.percona.com/files/presentations/VELOCITY2012-Beyond-the-Numbers.pdf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## muduo与Boost.Asio、libevent2的吞吐量对比\n",
    "\n",
    "我在编写muduo的时候并没有以高并发、高吞吐为主要目标。但出乎我的意料，ping pong测试表明，muduo的吞吐量比Boost.Asio高15%以上；比libevent2高18%以上，个别情况甚至达到70%.\n",
    "\n",
    "**测试对象**\n",
    "- boost 1.40中的asio 1.4.3\n",
    "- asio 1.4.5(http://think-async.com/Asio/Download)\n",
    "- libevent 2.0.6-rc(http://monkey.org/~provos/libevent-2.0.6-rc.tar.gz)\n",
    "- muduo 0.1.1\n",
    "\n",
    "**测试代码**\n",
    "- asio的测试代码取自 `http://asio.cvs.sourceforge.net/viewvc/asio/asio/src/tests/performance/`，未做更改。\n",
    "- 我自己编写了libevent2的 ping pong测试代码，路径是 `recipes/pingpong/libevent/`。由于这个测试代码没有使用多线程，所以只对比muduo和libevent2在单线程下的性能。\n",
    "- muduo的测试代码位于[`examples/pingpong/`](../muduo/muduo/examples/pingpong/)，代码如[gist](https://gist.github.com/564985)所示。\n",
    "\n",
    "```shell\n",
    "$ BUILD_TYPE=release ./build.sh # 编译muduo的优化版本\n",
    "```\n",
    "\n",
    "**测试环境**\n",
    "- 硬件：DELL490工作站，双路Intel四核XeonE5320CPU，共8核，主频1.86GHz，内存16GiB。\n",
    "- 软件：操作系统为Ubuntu Linux Server 10.04.1 LTS x86_64，编译器是g++4.4.3。\n",
    "\n",
    "**测试方法**\n",
    "\n",
    "依据asio性能测试 注 的办法，用ping pong协议来测试muduo、asio、libevent2在单机上的吞吐量。\n",
    "简单地说，ping pong协议是客户端和服务器都实现echo协议。当TCP连接建立时，客户端向服务器发送一些数据，服务器会echo回这些数据，然后客户端再echo回服务器。这些数据就会像乒乓球一样在客户端和服务器之间来回传送，直到有一方断开连接为止。这是用来测试吞吐量的常用办法。注意数据是无格式的，双方都是收到多少数据就反射回去多少数据，并不拆包，这与后面的ZeroMQ延迟测试不同。\n",
    "\n",
    "我主要做了两项测试：\n",
    "\n",
    "- 单线程测试。客户端与服务器运行在同一台机器，均为单线程，测试并发连接数为1/10/100/1000/10000时的吞吐量。\n",
    "- 多线程测试。并发连接数为100或1000，服务器和客户端的线程数同时设为1/2/3/4。（由于我家里只有一台8核机器，而且服务器和客户端运行在同一台机器上，线程数大于4没有意义。）\n",
    "\n",
    "在所有测试中，ping pong消息的大小均为16KiB。测试用的shell脚本可以从http://gist.github.com/564985下载。\n",
    "\n",
    "在同一台机器测试吞吐量的原因如下：<br/>\n",
    "现在的CPU很快，即便是单线程单TCP连接也能把千兆以太网的带宽跑满。如果用两台机器，所有的吞吐量测试结果将是110MiB/s，失去了对比的意义。（用Python也能跑出同样的吞吐量，或许可以对比哪个库占的CPU少。）\n",
    "\n",
    "在同一台机器上测试，可以在CPU资源相同的情况下，单纯对比网络库的效率。也就是说在单线程下，服务器端和客户端各占满1个CPU，比较哪个库的吞吐量高。\n",
    "\n",
    "单线程测试的结果（见图6-3）​，数字越大越好。\n",
    "\n",
    "<img src=\"./images/6.3.png\" alt=\"6.3\" style=\"zoom:50%;\" />\n",
    "\n",
    "<img src=\"./images/6.4.png\" alt=\"6.4\" style=\"zoom:50%;\" />\n",
    "\n",
    "以上结果让人大跌眼镜，muduo居然比libevent2快70%！跟踪libevent2的源代码发现，它每次最多从socket读取4096字节的数据（证据在`buffer.c`的`evbuffer_read()`函数），怪不得吞吐量比muduo小很多。因为在这一测试中，muduo每次读取16384字节，系统调用的性价比较高。\n",
    "\n",
    "为了公平起见，我再测了一次，这回两个库都发送4096字节的消息（见图6-4）。\n",
    "\n",
    "测试结果表明muduo的吞吐量平均比libevent2高18%以上。\n",
    "\n",
    "多线程测试的结果（见图6-5），数字越大越好。\n",
    "\n",
    "<img src=\"./images/6.5.png\" alt=\"6.5\" style=\"zoom:50%;\" />\n",
    "\n",
    "测试结果表明muduo的吞吐量平均比asio高15%以上。\n",
    "\n",
    "**讨论**\n",
    "\n",
    "muduo出乎意料地比asio性能优越，我想主要得益于其简单的设计和简洁的代码。asio在多线程测试中表现不佳，我猜测其主要原因是测试代码只使用了一个`io_service`，如果改用“io_service per CPU”的话，其性能应该有所提高。我对asio的了解程度仅限于能读懂其代码，希望能有asio高手编写“io_service per CPU”的ping pong测试，以便与muduo做一个公平的比较。\n",
    "\n",
    "由于libevent2每次最多从网络读取4096字节，这大大限制了它的吞吐量。\n",
    "\n",
    "ping pong测试很容易实现，欢迎其他网络库（ACE、POCO、libevent等）也能加入到对比中来，期待这些库的高手出马。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 击鼓传花：对比muduo与libevent2的事件处理效率\n",
    "\n",
    "前面我们比较了muduo和libevent2的吞吐量，得到的结论是muduo比libevent2快18%。有人会说，libevent2并不是为高吞吐量的应用场景而设计的，这样的比较不公平，胜之不武。为了公平起见，这回我们用libevent2自带的性能测试程序（击鼓传花）来对比muduo和libevent2在高并发情况下的IO事件处理效率。\n",
    "\n",
    "测试用的软硬件环境与前一小节相同，另外我还在自己的DELL E6400笔记本电脑上运行了测试，结果也附在后面。\n",
    "\n",
    "测试的场景是：有1000个人围成一圈，玩击鼓传花的游戏，一开始第1个人手里有花，他把花传给右手边的人，那个人再继续把花传给右手边的人，当花转手100次之后游戏停止，记录从开始到结束的时间。\n",
    "\n",
    "用程序表达是，有1000个网络连接（`socketpair(2)`或`pipe(2)`），数据在这些连接中顺次传递，一开始往第1个连接里写1个字节，然后从这个连接的另一头读出这1个字节，再写入第2个连接，然后读出来继续写到第3个连接，直到一共写了100次之后程序停止，记录所用的时间。\n",
    "\n",
    "以上是只有一个活动连接的场景，我们实际测试的是100个或1000个活动连接（即100朵花或1000朵花，均匀分散在人群手中），而连接总数（即并发数）从100~100000（10万）。注意每个连接是两个文件描述符，为了运行测试，需要调高每个进程能打开的文件数，比如设为256000。\n",
    "\n",
    "libevent2的测试代码位于`test/bench.c`，我修复了2.0.6-rc版里的一个小bug。修正后的代码见已经提交给libevent2作者，现在下载的最新版是正确的。\n",
    "\n",
    "muduo的测试代码位于[`examples/pingpong/bench.cc`](./pingpong/bench.cc)。\n",
    "\n",
    "**测试结果与讨论**\n",
    "\n",
    "第一轮，分别用100个活动连接和1000个活动连接，无超时，读写100次，测试一次游戏的总时间（包含初始化）和事件处理的时间（不包含注册event watcher）随连接数（并发数）变化的情况。具体解释见libev的性能测试文档 注，不同之处在于我们不比较timer event的性能，只比较IO event的性能。对每个并发数，程序循环25次，剔去第一次的热身数据，后24次算平均值。测试用的[脚本](../recipes/pingpong/libevent/run_bench.sh)是libev的作者Marc Lehmann写的，我略做改用，用于测试muduo和libevent2。\n",
    "> - http://libev.schmorp.de/bench.html\n",
    "\n",
    "第一轮的结果（见图6-6），请先只看“+”线（实线）和“×”线（粗虚线）。“×”线是libevent2用的时间，“+”线是muduo用的时间。数字越小越好。注意这个图的横坐标是对数的，每一个数量级的取值点为1，2，3，4，5，6，7.5，10。\n",
    "\n",
    "<img src=\"./images/6.6.png\" alt=\"6.6\" style=\"zoom:50%;\" />\n",
    "\n",
    "从两条线的对比可以看出：\n",
    "\n",
    "1. libevent2在初始化event watcher方面比muduo快20%（左边的两个图）。\n",
    "2. 在事件处理方面（右边的两个图）\n",
    "   - a. 在100个活动连接的情况下，\n",
    "     - 当总连接数（并发数）小于1000或大于30000时，二者性能差不多；\n",
    "     - 当总连接数大于1000或小于30000时，libevent2明显领先。\n",
    "   - b. 在1000个活动连接的情况下，\n",
    "     - 当并发数小于10000时，libevent2和muduo得分接近；\n",
    "     - 当并发数大于10000时，muduo明显占优。\n",
    "\n",
    "这里有两个问题值得探讨：\n",
    "\n",
    "1. 为什么muduo花在初始化上的时间比较多？\n",
    "2. 为什么在一些情况下它比libevent2慢很多？\n",
    "\n",
    "我仔细分析了其中的原因，并参考了libev的作者Marc Lehmann的观点 注，结论是：在第一轮初始化时，libevent2和muduo都是用`epoll_ctl(fd, EPOLL_CTL_ADD, ...)`来添加文件描述符的event watcher。不同之处在于，在后面24轮中，muduo使用了`epoll_ctl(fd, EPOLL_CTL_MOD, ...)`来更新已有的event watcher；然而libevent2继续调用`epoll_ctl(fd, EPOLL_CTL_ADD, ...)`来重复添加fd，并忽略返回的错误码`EEXIST`(File exists)。在这种重复添加的情况下，`EPOLL_CTL_ADD`将会快速地返回错误，而`EPOLL_CTL_MOD`会做更多的工作，花的时间也更长。于是libevent2捡了个便宜。\n",
    "\n",
    "为了验证这个结论，我改动了muduo，让它每次都用`EPOLL_CTL_ADD`方式初始化和更新event watcher，并忽略返回的错误。\n",
    "\n",
    "第二轮测试结果见图6-6的细虚线，可见改动之后的muduo的初始化性能比libevent2更好，事件处理的耗时也有所降低（我推测是kernel内部的原因）。\n",
    "\n",
    "这个改动只是为了验证想法，我并没有把它放到muduo最终的代码中去，这或许可以留作日后优化的余地。（具体的改动是`muduo/net/poller/EPollPoller.cc`第138行和173行，读者可自行验证。）\n",
    "\n",
    "同样的测试在双核笔记本电脑上运行了一次，结果如图6-7所示。（我的笔记本电脑的CPU主频是2.4GHz，高于台式机的1.86GHz，所以用时较少。）\n",
    "\n",
    "<img src=\"./images/6.7.png\" alt=\"6.7\" style=\"zoom:50%;\" />\n",
    "\n",
    "结论：在事件处理效率方面，muduo与libevent2总体比较接近，各擅胜场。在并发量特别大的情况下（大于10000）​，muduo略微占优。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## muduo与Nginx的吞吐量对比\n",
    "\n",
    "本节简单对比了Nginx 1.0.12和muduo 0.3.1内置的简陋HTTP服务器的长连接性能。其中muduo的HTTP实现和测试代码位于muduo/net/http/。\n",
    "\n",
    "**测试环境**\n",
    "- 服务端，运行HTTP server，8核DELL 490工作站，Xeon E5320 CPU。\n",
    "- 客户端，运行ab 注 和weighttp 注，4核i5-2500 CPU。\n",
    "- 网络：普通家用千兆网。\n",
    "\n",
    "测试方法 为了公平起见，Nginx和muduo都没有访问文件，而是直接返回内存中的数据。毕竟我们想比较的是程序的网络性能，而不是机器的磁盘性能。另外，这里客户端的性能优于服务机，因为我们要给服务端HTTP server施压，试图使其饱和，而不是测试HTTP client的性能。\n",
    "\n",
    "muduo HTTP测试服务器的主要代码：\n",
    "\n",
    "- [HttpServer_test.cc](../muduo/muduo/net/http/tests/HttpServer_test.cc)\n",
    "\n",
    "Nginx使用了章亦春的HTTP echo模块来实现直接返回数据。[配置文件如下](https://gist.github.com/1967026)：\n",
    "> https://nginxtutorials.com/nginx-http-echo-module/\n",
    "\n",
    "客户端运行以下命令来获取/hello的内容，服务器返回字符串\"hello, world!\"。\n",
    "\n",
    "```bash\n",
    "./ab-n100000-k-r-c100010.0.0.9：8080/hello\n",
    "```\n",
    "\n",
    "先测试单线程的性能（见图6-8），横轴是并发连接数，纵轴为每秒完成的HTTP请求响应数目，下同。在测试期间，ab的CPU使用率低于70%，客户端游刃有余。\n",
    "\n",
    "<img src=\"./images/6.8.png\" alt=\"6.8\" style=\"zoom:50%;\" />\n",
    "\n",
    "再对比muduo 4线程和Nginx 4工作进程的性能（见图6-9）​。当连接数大于20时，`top(1)`显示ab的CPU使用率达到85%，已经饱和，因此换用weighttp（双线程）来完成其余测试。\n",
    "\n",
    "<img src=\"./images/6.9.png\" alt=\"6.9\" style=\"zoom:50%;\" />\n",
    "\n",
    "CPU使用率对比（百分比是top(1)显示的数值）：\n",
    "\n",
    "- 10000并发连接，4workers/threads，muduo是4×83%，Nginx是4×75%\n",
    "- 1000并发连接，4 workers/threads，muduo是4×85%，Nginx是4×78%\n",
    "\n",
    "初看起来Nginx的CPU使用率略低，但是实际上二者都已经把CPU资源耗尽了。与CPU benchmark不同，涉及IO的benchmark在满负载下的CPU使用率不会达到100%，因为内核要占用一部分时间处理IO。这里的数值差异说明muduo和Nginx在满负荷的情况下，用户态和内核态的比重略有区别。\n",
    "\n",
    "测试结果显示muduo多数情况下略快，Nginx和muduo在合适的条件下qps（每秒请求数）都能超过10万。值得说明的是，muduo没有实现完整的HTTP服务器，而只是实现了满足最基本要求的HTTP协议，因此这个测试结果并不是说明muduo比Nginx更适合用做httpd，而是说明muduo在性能方面没有犯低级错误。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## muduo与ZeroMQ的延迟对比\n",
    "\n",
    "本节我们用ZeroMQ自带的延迟和吞吐量测试与muduo做一对比，muduo代码位于[examples/zeromq/](../muduo/muduo/examples/zeromq/)。测试的内容很简单，可以认为是§ 6.5.1 ping pong测试的翻版，不同之处在于这里的消息的长度是固定的，收到完整的消息再echo回发送方，如此往复。测试结果如图6-10所示，横轴为消息的长度，纵轴为单程延迟（微秒）​。可见在消息长度小于16KiB时，muduo的延迟稳定地低于ZeroMQ。\n",
    "\n",
    "<img src=\"./images/6.10.png\" alt=\"6.10\" style=\"zoom:50%;\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 6.6 详解muduo多线程模型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本节以一个Sudoku Solver为例，回顾了并发网络服务程序的多种设计方案，并介绍了使用muduo网络库编写多线程服务器的两种最常用手法。下一章的例子展现了muduo在编写单线程并发网络服务程序方面的能力与便捷性。今天我们先看一看它在多线程方面的表现。本节代码参见：[examples/sudoku/](https://github.com/chenshuo/muduo/tree/master/examples/sudoku)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.6.1 数独求解服务器"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "假设有这么一个网络编程任务：写一个求解数独的程序(Sudoku Solver)，并把它做成一个网络服务。\n",
    "\n",
    "Sudoku Solver是我喜爱的网络编程例子，它曾经出现在“分布式系统部署、监控与进程管理的几重境界”(§ 9.8)、​“muduo Buffer类的设计与使用”(§ 7.4)、​“‘多线程服务器的适用场合’例释与答疑”(§ 3.6)等处，它也可以看成是echo服务的一个变种（附录A“谈一谈网络编程学习经验”把echo列为三大TCP网络编程案例之一）​。\n",
    "\n",
    "写这么一个程序在网络编程方面的难度不高，跟写echo服务差不多（从网络连接读入一个Sudoku题目，算出答案，再发回给客户）​，挑战在于怎样做才能发挥现在多核硬件的能力？在谈这个问题之前，让我们先写一个基本的单线程版。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 协议"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "一个简单的以`\\r\\n`分隔的文本行协议，使用TCP长连接，客户端在不需要服务时主动断开连接。\n",
    "- 请求：`​[id：​]<81digits>\\r\\n`\n",
    "- 响应：`​[id：​]<81digits>\\r\\n`\n",
    "- 或者：`​[id：​]NoSolution\\r\\n`\n",
    "\n",
    "其中`[id：​]`表示可选的`id`，用于区分先后的请求，以支持Parallel Pipelining，响应中会回显请求中的`id`。Parallel Pipelining的意义见[赖勇浩的《以小见大——那些基于Protobuf的五花八门的RPC(2)》](https://blog.csdn.net/gzlaiyonghao/article/details/6316099)，或者见我写的[《分布式系统的工程化开发方法》](https://blog.csdn.net/solstice/article/details/5950190)第54页关于out-of-order RPC的介绍。\n",
    "> - https://www.cnblogs.com/aiwz/archive/2011/04/11/6154347.html\n",
    "\n",
    "`<81digits>`是Sudoku的棋盘，9×9个数字，从左上角到右下角按行扫描，未知数字以`0`表示。如果Sudoku有解，那么响应是填满数字的棋盘；如果无解，则返回`NoSolution`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 例子1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 请求：`000000010400000000020000000000050407008000300001090000300400200050100000000806000\\r\\n`\n",
    "- 响应：`693784512487512936125963874932651487568247391741398625319475268856129743274836159\\r\\n`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 例子2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 请求：`a：000000010400000000020000000000050407008000300001090000300400200050100000000806000\\r\\n`\n",
    "- 响应：`a：693784512487512936125963874932651487568247391741398625319475268856129743274836159\\r\\n`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 例子3"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 请求：`b：000000010400000000020000000000050407008000300001090000300400200050100000000806005\\r\\n`\n",
    "- 响应：`b：NoSolution\\r\\n`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 基本实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sudoku的求解算法见[《谈谈数独(Sudoku)》](https://blog.csdn.net/Solstice/article/details/2096209)一文，这不是本文的重点。假设我们已经有一个函数能求解`Sudoku`，它的原型如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "string solveSudoku(const string& puzzle)；"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "函数的输入是上文的“<81digits>”​，输出是“<81digits>”或“NoSolution”​。这个函数是个pure function，同时也是线程安全的。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "有了这个函数，我们以§ 6.4.2“echo服务的实现”中出现的`EchoServer`为蓝本，稍加修改就能得到`SudokuServer`。这里只列出最关键的`onMessage()`函数，完整的代码见[examples/sudoku/server_basic.cc](./sudoku/server_basic.cc)。`onMessage()`的主要功能是处理协议格式，并调用`solveSudoku()`求解问题。这个函数应该能正确处理TCP分包。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- [examples/sudoku/server_basic.cc](./sudoku/server_basic.cc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`server_basic.cc`是一个并发服务器，可以同时服务多个客户连接。但是它是单线程的，无法发挥多核硬件的能力。\n",
    "\n",
    "Sudoku是一个计算密集型的任务（见§ 7.4中关于其性能的分析）​，其瓶颈在CPU。为了让这个单线程`server_basic`程序充分利用CPU资源，一个简单的办法是在同一台机器上部署多个`server_basic`进程，让每个进程占用不同的端口，比如在一台8核机器上部署8个`server_basic`进程，分别占用`9981，9982，…，9988`端口。这样做其实是把难题推给了客户端，因为客户端(s)要自己做负载均衡。再想得远一点，在8个`server_basic`前面部署一个`load balancer?`似乎小题大做了。\n",
    "\n",
    "能不能在一个端口上提供服务，并且又能发挥多核处理器的计算能力呢？当然可以，办法不止一种。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.6.2 常见的并发网络服务程序设计方案"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "W.Richard Stevens的《UNIX网络编程（第2版）》第27章“Client-Server Design Alternatives”介绍了十来种当时（20世纪90年代末）流行的编写并发网络程序的方案。[UNP]第3版第30章，内容未变，还是这几种。以下简称UNP CSDA方案。[UNP]这本书主要讲解阻塞式网络编程，在非阻塞方面着墨不多，仅有一章。正确使用non-blocking IO需要考虑的问题很多，不适宜直接调用Sockets API，而需要一个功能完善的网络库支撑。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "随着2000年前后第一次互联网浪潮的兴起，业界对高并发HTTP服务器的强烈需求大大推动了这一领域的研究，目前高性能httpd普遍采用的是单线程Reactor方式。另外一个说法是IBM Lotus使用TCP长连接协议，而把Lotus服务端移植到Linux的过程中IBM的工程师们大大提高了Linux内核在处理并发连接方面的可伸缩性，因为一个公司可能有上万人同时上线，连接到同一台跑着Lotus Server的Linux服务器。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可伸缩网络编程这个领域其实近十年来没什么新东西，POSA2已经进行了相当全面的总结，另外以下几篇文章也值得参考。\n",
    "- http://bulk.fefe.de/scalable-networking.pdf\n",
    "- http://www.kegel.com/c10k.html\n",
    "- http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "表是笔者总结的12种常见方案。\n",
    "\n",
    "其中“互通”指的是如果开发chat服务，多个客户连接之间是否能方便地交换数据（chat也是附录A中举的三大TCP网络编程案例之一）。对于`echo/httpd/Sudoku`这类“连接相互独立”的服务程序，这个功能无足轻重，但是对于chat类服务却至关重要。“顺序性”指的是在httpd/Sudoku这类请求响应服务中，如果客户连接顺序发送多个请求，那么计算得到的多个响应是否按相同的顺序发还给客户（这里指的是在自然条件下，不含刻意同步）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| 方案 | 并发模型                  | [UNP]对应 | 多进程 | 多线程 | 阻塞IO | IO复用 | 长连接 | 并发性 | 多核 | 开销 | 互通 | 顺序性 | 线程数 | 特点                         |\n",
    "| ---- | ------------------------- | --------- | ------ | ------ | ------ | ------ | ------ | ------ | ---- | ---- | ---- | ------ | ------ | ---------------------------- |\n",
    "| 0    | accept+read/write         | 0         | 否     | 否     | 是     | 否     | 否     | 无     | 否   | 低   | 否   | 是     | 常     | 一次服务一个客户             |\n",
    "| 1    | accept+fork               | 1         | 是     | 否     | 是     | 否     | 是     | 低     | 是   | 高   | 否   | 是     | 变     | process-per-connection       |\n",
    "| 2    | accept+thread             | 6         | 否     | 是     | 是     | 否     | 是     | 中     | 是   | 中   | 是   | 是     | 变     | thread-per-connection        |\n",
    "| 3    | prefork                   | 2/3/4/5   | 是     | 否     | 是     | 否     | 是     | 低     | 是   | 高   | 否   | 是     | 变     | 见[UNP]                      |\n",
    "| 4    | pre threaded              | 7/8       | 否     | 是     | 是     | 否     | 是     | 中     | 是   | 中   | 是   | 是     | 变     | 见[UNP]                      |\n",
    "| 5    | poll (reactor)            | 6.8节     | 否     | 否     | 否     | 是     | 是     | 高     | 否   | 低   | 是   | 是     | 常     | 单线程reactor                |\n",
    "| 6    | reactor + thread-per-task | 无        | 否     | 是     | 否     | 是     | 是     | 中     | 是   | 中   | 是   | 否     | 变     | thread-per-request           |\n",
    "| 7    | reactor + worker thread   | 无        | 否     | 是     | 否     | 是     | 是     | 中     | 是   | 中   | 是   | 是     | 变     | worker-thread-per-connection |\n",
    "| 8    | reactor + thread poll     | 无        | 否     | 是     | 否     | 是     | 是     | 高     | 是   | 低   | 是   | 否     | 常     | 主线程IO，工作线程计算       |\n",
    "| 9    | reactors in threads       | 无        | 否     | 是     | 否     | 是     | 是     | 高     | 是   | 低   | 是   | 是     | 常     | one loop per thread          |\n",
    "| 10   | reactors in processes     | 无        | 是     | 否     | 否     | 是     | 是     | 高     | 是   | 低   | 否   | 是     | 常     | Nginx                        |\n",
    "| 11   | reactors + thread pool    | 无        | 否     | 是     | 否     | 是     | 是     | 高     | 是   | 低   | 是   | 否     | 常     | 最灵活的IO与CPU配置          |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- AI\n",
    "\n",
    "| 方案                      | 描述              | 优点                       | 缺点                           | 适用场景           |\n",
    "| ------------------------- | ----------------- | -------------------------- | ------------------------------ | ------------------ |\n",
    "| accept + read/write       | 单线程顺序处理    | 实现简单，无并发问题       | 并发度低，阻塞严重             | 玩具级Demo         |\n",
    "| accept + fork             | 每个连接一个进程  | 实现简单，进程间隔离       | 进程创建开销大，系统资源消耗大 | 并发连接少的场景   |\n",
    "| accept + thread           | 每个连接一个线程  | 实现相对简单，资源共享方便 | 线程开销，频繁创建销毁影响性能 | 并发连接适中的场景 |\n",
    "| prefork                   | 预先fork多个进程  | 避免动态创建进程的开销     | 连接分配不均衡，进程间通信复杂 | Apache服务器       |\n",
    "| pre threaded              | 预先创建线程池    | 避免线程频繁创建销毁       | 任务分配开销，连接数限制       | 连接数可预测场景   |\n",
    "| poll(reactor)             | 单线程Reactor模式 | 资源消耗小，实现简单       | 无法利用多核，容易阻塞         | Redis早期版本      |\n",
    "| reactor + thread-per-task | 任务级线程池      | 可以利用多核，响应及时     | 需要考虑任务分配和线程同步     | 计算密集型服务     |\n",
    "| reactor + worker thread   | 固定工作线程      | 线程管理简单，负载均衡好   | 任务队列可能成为瓶颈           | muduo网络库        |\n",
    "| reactor + thread pool     | 反应堆+线程池     | 灵活性好，性能强           | 实现复杂，调试困难             | 高性能服务器       |\n",
    "| reactors in threads       | 多Reactor多线程   | 充分利用多核，扩展性好     | 实现最复杂                     | Nginx、Netty       |\n",
    "| reactors in processs      | 多Reactor多进程   | 进程间隔离，稳定性好       | 进程间通信开销                 | 大型服务器         |\n",
    "| reactors + thread pool    | Reactor主从模式   | 伸缩性好，性能强           | 实现和调试复杂                 | ACE框架            |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "UNP CSDA方案归入0～5。\n",
    "- 方案5也是目前用得很多的单线程Reactor方案，muduo对此提供了很好的支持。\n",
    "- 方案6和方案7其实不是实用的方案，只是作为过渡品。\n",
    "- 方案8和方案9是本文重点介绍的方案，其实这两个方案已经在§ 3.3“多线程服务器的常用编程模型”中提到过，只不过当时没有用具体的代码示例来说明。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案0: accept + read/write"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这其实不是并发服务器，而是iterative服务器，因为它一次只能服务一个客户。\n",
    "\n",
    "代码见[UNP]中的Figure 1.9，​[UNP]以此为对比其他方案的基准点。这个方案不适合长连接，倒是很适合daytime这种write-only短连接服务。\n",
    "\n",
    "以下Python代码展示用方案0实现echo server的大致做法(本章的Python代码均没有考虑错误处理):"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- https://github.com/chenshuo/recipes/blob/master/python/echo-iterative.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案1: accept + fork\n",
    "\n",
    "方案1 这是传统的Unix并发网络编程方案，​[UNP]称之为child-per-client或fork()-per-client，另外也俗称process-per-connection。\n",
    "\n",
    "这种方案适合并发连接数不大的情况。至今仍有一些网络服务程序用这种方式实现，比如PostgreSQL和Perforce的服务端。\n",
    "\n",
    "这种方案适合“计算响应的工作量远大于 fork() 的开销”这种情况，比如数据库服务器。这种方案适合长连接，但不太适合短连接，因为`fork()`开销大于求解Sudoku的用时。\n",
    "\n",
    "- https://github.com/chenshuo/recipes/blob/master/python/echo-fork.py\n",
    "\n",
    "在这种编程方式中，业务逻辑已经初步从网络框架分离出来，但是仍然和IO紧密结合"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案2: accept + thread\n",
    "\n",
    "这是传统的Java网络编程方案thread-per-connection，在Java 1.4引入NIO之前，Java网络服务多采用这种方案。它的初始化开销比方案1要小很多，但与求解Sudoku的用时差不多，仍然不适合短连接服务。\n",
    "\n",
    "这种方案的伸缩性受到线程数的限制，一两百个还行，几千个的话对操作系统的scheduler恐怕是个不小的负担。\n",
    "\n",
    "- https://github.com/chenshuo/recipes/blob/master/python/echo-thread.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案3: prefork\n",
    "\n",
    "针对方案1的优化，​[UNP]详细分析了几种变化，包括对accept(2)“惊群”问题(thundering herd)的考虑。\n",
    "\n",
    "- https://github.com/chenshuo/recipes/blob/master/python/echo-single.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案4: pre threaded\n",
    "\n",
    "对方案2的优化，​[UNP]详细分析了它的几种变化。方案3和方案4这两个方案都是Apache httpd长期使用的方案"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以上几种方案都是阻塞式网络编程，程序流程(thread of control)通常阻塞在`read()`上，等待数据到达。\n",
    "\n",
    "但是TCP是个全双工协议，同时支持`read()`和`write()`操作，当一个线程/进程阻塞在`read()`上，但程序又想给这个TCP连接发数据，那该怎么办？比如说echo client，既要从`stdin`读，又要从网络读，当程序正在阻塞地读网络的时候，如何处理键盘输入？\n",
    "\n",
    "又比如proxy，既要把连接a收到的数据发给连接b，又要把从b收到的数据发给a，那么到底读哪个？​（proxy是附录A讲的三大TCP网络编程案例之一。​）\n",
    "\n",
    "一种方法是用两个线程/进程，一个负责读，一个负责写。​[UNP]也在实现echo client时介绍了这种方案。§ 7.13举了一个Python双线程TCP relay的例子，另外见Python Pinhole的代码：https://code.activestate.com/recipes/114642-pinhole/。\n",
    "\n",
    "另一种方法是使用IO multiplexing，也就是`select/poll/epoll/kqueue`这一系列的“多路选择器”​，让一个thread of control能处理多个连接。​**“IO复用”其实复用的不是IO连接，而是复用线程**。使用`select/poll`几乎肯定要配合non-blocking IO，而使用non-blocking IO肯定要使用应用层`buffer`，原因见§ 7.4。这就不是一件轻松的事儿了，如果每个程序都去搞一套自己的IO multiplexing机制（本质是event-driven事件驱动）​，这是一种很大的浪费。感谢*Doug Schmidt为我们总结出了Reactor模式*，让event-driven网络编程有章可循。继而出现了一些通用的Reactor框架/库，比如libevent、muduo、Netty、twisted、POE等等。有了这些库，我想基本不用去编写阻塞式的网络程序了（特殊情况除外，比如proxy流量限制）​。\n",
    "\n",
    "这里先用一小段Python代码简要地回顾“以IO multiplexing方式实现并发echo server”的基本做法。为了简单起见，以下代码并没有开启non-blocking，也没有考虑数据发送不完整(L28)等情况。首先定义一个从文件描述符到socket对象的映射(L14)，程序的主体是一个事件循环(L15～L32)，每当有IO事件发生时，就针对不同的文件描述符(fileno)执行不同的操作(L16，L17)。对于listening fd，接受(accept)新连接，并注册到IO事件关注列表(watch list)，然后把连接添加到connections字典中(L18～L23)。对于客户连接，则读取并回显数据，并处理连接的关闭(L24～L32)。对于echo服务而言，真正的业务逻辑只有L28：将收到的数据原样发回客户端。\n",
    "\n",
    "- https://github.com/chenshuo/recipes/blob/master/python/echo-poll.py\n",
    "\n",
    "注意以上代码不是功能完善的IO multiplexing范本，它没有考虑错误处理，也没有实现定时功能，而且只适合侦听(`listen`)一个端口的网络服务程序。如果需要侦听多个端口，或者要同时扮演客户端，那么代码的结构需要推倒重来。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个代码骨架可用于实现多种TCP服务器。例如写一个聊天服务只需改动3行代码，如下所示。业务逻辑是L28～L30：将本连接收到的数据转发给其他客户连接。\n",
    "\n",
    "- https://github.com/chenshuo/recipes/blob/master/python/chat-poll.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是这种把业务逻辑隐藏在一个大循环中的做法其实不利于将来功能的扩展，我们能不能设法把业务逻辑抽取出来，与网络基础代码分离呢？\n",
    "\n",
    "Doug Schmidt指出，其实网络编程中有很多是事务性(routine)的工作，可以提取为公用的框架或库，而用户只需要填上关键的业务逻辑代码，并将回调注册到框架中，就可以实现完整的网络服务，这正是Reactor模式的主要思想。\n",
    "\n",
    "如果用传统Windows GUI消息循环来做一个类比，那么我们前面展示IO multi-plexing的做法相当于把程序的全部逻辑都放到了窗口过程(WndProc)的一个巨大的switch-case语句中，这种做法无疑是不利于扩展的。​（各种GUI框架在此各显神通。​）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)\n",
    "{\n",
    "    switch (message)\n",
    "    {\n",
    "        case WM_DESTROY:\n",
    "            PostQuitMessage(0);\n",
    "            return 0;\n",
    "        // many more cases\n",
    "    }\n",
    "    return DefWindowProc(hwnd, message, wParam, lParam);\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**而Reactor的意义在于将消息（IO事件）分发到用户提供的处理函数，并保持网络部分的通用代码不变，独立于用户的业务逻辑**。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "单线程Reactor的程序执行顺序如图6-11（左图）所示。在没有事件的时候，线程等待在`select/poll/epoll_wait`等函数上。事件到达后由网络库处理IO，再把消息通知（回调）客户端代码。Reactor事件循环所在的线程通常叫IO线程。通常由网络库负责读写socket，用户代码负载解码、计算、编码。\n",
    "\n",
    "注意由于只有一个线程，因此事件是顺序处理的，一个线程同时只能做一件事情。在这种协作式多任务中，事件的优先级得不到保证，因为从“poll返回之后”到“下一次调用poll进入等待之前”这段时间内，线程不会被其他连接上的数据或事件抢占（见图6-11的右图）​。如果我们想要延迟计算（把`compute()`推迟100ms）​，那么也不能用`sleep()`之类的阻塞调用，而应该注册超时回调，以避免阻塞当前IO线程。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./images/6.11.png\" alt=\"6.11\" style=\"zoom:50%;\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案5: poll(reactor)\n",
    "\n",
    "方案5 基本的单线程Reactor方案（见图6-11）​，即前面的[server_basic.cc](./sudoku/server_basic.cc)程序。本文以它作为对比其他方案的基准点。这种方案的优点是由网络库搞定数据收发，程序只关心业务逻辑；缺点在前面已经谈了：适合IO密集的应用，不太适合CPU密集的应用，因为较难发挥多核的威力。另外，与方案2相比，方案5处理网络消息的延迟可能要略大一些，因为方案2直接一次`read(2)`系统调用就能拿到请求数据，而方案5要先`poll(2)`再`read(2)`，多了一次系统调用。\n",
    "\n",
    "这里用一小段Python代码展示Reactor模式的雏形。为了节省篇幅，这里直接使用了全局变量，也没有处理异常。程序的核心仍然是事件循环(L42～L46)，与前面不同的是，事件的处理通过`handlers`转发到各个函数中，不再集中在一坨。例如l`istening fd`的处理函数是`handle_accept`，它会注册客户连接的`handler`。普通客户连接的处理函数是`handle_request`，其中又把连接断开和数据到达这两个事件分开，后者由`handle_input`处理。业务逻辑位于单独的`handle_input`函数，实现了分离。\n",
    "\n",
    "- https://github.com/chenshuo/recipes/blob/master/python/echo-reactor.py\n",
    "\n",
    "如果要改成聊天服务，重新定义handle_input函数即可，程序的其余部分保持不变。\n",
    "\n",
    "- https://github.com/chenshuo/recipes/blob/master/python/chat-reactor.py\n",
    "\n",
    "必须说明的是，完善的非阻塞IO网络库远比上面的玩具代码复杂，需要考虑各种错误场景。特别是要真正接管数据的收发，而不是像上面的示例那样直接在事件处理回调函数中发送网络数据。\n",
    "\n",
    "注意在使用非阻塞IO+事件驱动方式编程的时候，一定要注意避免在事件回调中执行耗时的操作，包括阻塞IO等，否则会影响程序的响应。这和Windows GUI消息循环非常类似。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案6: reactor + thread-per-task\n",
    "\n",
    "方案6 这是一个过渡方案，收到Sudoku请求之后，不在Reactor线程计算，而是创建一个新线程去计算，以充分利用多核CPU。这是非常初级的多线程应用，因为它为每个请求（而不是每个连接）创建了一个新线程。这个开销可以用线程池来避免，即方案8。这个方案还有一个特点是out-of-order，即同时创建多个线程去计算同一个连接上收到的多个请求，那么算出结果的次序是不确定的，可能第2个Sudoku比较简单，比第1个先算出结果。这也是我们在一开始设计协议的时候使用了id的原因，以便客户端区分response对应的是哪个request。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案7: reactor + worker thread\n",
    "\n",
    "方案7 为了让返回结果的顺序确定，我们可以为每个连接创建一个计算线程，每个连接上的请求固定发给同一个线程去算，先到先得。这也是一个过渡方案，因为并发连接数受限于线程数目，这个方案或许还不如直接使用阻塞IO的thread-per-connection方案2。\n",
    "\n",
    "方案7与方案6的另外一个区别是单个client的最大CPU占用率。在方案6中，一个TCP连接上发来的一长串突发请求(burst requests)可以占满全部8个core；而在方案7中，由于每个连接上的请求固定由同一个线程处理，那么它最多占用12.5%的CPU资源。这两种方案各有优劣，取决于应用场景的需要（到底是公平性重要还是突发性能重要）​。这个区别在方案8和方案9中同样存在，需要根据应用来取舍。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案8: reactor + thread poll\n",
    "\n",
    "方案8 为了弥补方案6中为每个请求创建线程的缺陷，我们使用固定大小线程池，程序结构如图6-12所示。全部的IO工作都在一个Reactor线程完成，而计算任务交给thread pool。如果计算任务彼此独立，而且IO的压力不大，那么这种方案是非常适用的。Sudoku Solver正好符合。代码参见：[examples/sudoku/server_threadpool.cc](./sudoku/server_threadpool.cc)。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./images/6.12.png\" alt=\"6.12\" style=\"zoom:50%;\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "方案8使用线程池的代码与单线程Reactor的方案5相比变化不大，只是把原来`onMessage()`中涉及计算和发回响应的部分抽出来做成一个函数，然后交给ThreadPool去计算。记住方案8有乱序返回的可能，客户端要根据id来匹配响应。\n",
    "\n",
    "线程池的另外一个作用是执行阻塞操作。比如有的数据库的客户端只提供同步访问，那么可以把数据库查询放到线程池中，可以避免阻塞IO线程，不会影响其他客户连接，就像Java Servlet 2.x的做法一样。另外也可以用线程池来调用一些阻塞的IO函数，例如`fsync(2)`/`fdatasync(2)`，这两个函数没有非阻塞的版本。\n",
    "\n",
    "> 不过目前Linux内核的实现仍然会阻塞其他线程的磁盘IO，见http://antirez.com/post/fsync-different-thread-useless.html。\n",
    "\n",
    "如果IO的压力比较大，一个Reactor处理不过来，可以试试方案9，它采用多个Reactor来分担负载。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案9: reactors in threads\n",
    "\n",
    "方案9 这是muduo内置的多线程方案，也是Netty内置的多线程方案。这种方案的特点是**one loop per thread**，有一个main Reactor负责`accept(2)`连接，然后把连接挂在某个sub Reactor中（muduo采用round-robin的方式来选择sub Reactor）​，这样该连接的所有操作都在那个sub Reactor所处的线程中完成。多个连接可能被分派到多个线程中，以充分利用CPU。\n",
    "\n",
    "muduo采用的是固定大小的Reactor pool，池子的大小通常根据CPU数目确定，也就是说线程数是固定的，这样程序的总体处理能力不会随连接数增加而下降。另外，由于一个连接完全由一个线程管理，那么请求的顺序性有保证，突发请求也不会占满全部8个核（如果需要优化突发请求，可以考虑方案11）​。这种方案把IO分派给多个线程，防止出现一个Reactor的处理能力饱和。\n",
    "\n",
    "与方案8的线程池相比，方案9减少了进出thread pool的两次上下文切换，在把多个连接分散到多个Reactor线程之后，小规模计算可以在当前IO线程完成并发回结果，从而降低响应的延迟。我认为这是一个适应性很强的多线程IO模型，因此把它作为muduo的默认线程模型（见图6-13）​。\n",
    "\n",
    "方案9代码见：[examples/sudoku/server_multiloop.cc](./sudoku/server_multiloop.cc)。它与server_basic.cc的区别很小，最关键的只有一行代码：server_.setThreadNum(numThreads)；"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./images/6.13.png\" alt=\"6.13\" style=\"zoom:50%;\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 方案10: reactors in processs\n",
    "\n",
    "方案10 这是Nginx的内置方案。如果连接之间无交互，这种方案也是很好的选择。工作进程之间相互独立，可以热升级。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "source": [
    "### 方案11: reactors + thread pool\n",
    "\n",
    "方案11 把方案8和方案9混合，既使用多个Reactor来处理IO，又使用线程池来处理计算。这种方案适合既有突发IO（利用多线程处理多个连接上的IO）​，又有突发计算的应用（利用线程池把一个连接上的计算任务分配给多个线程去做）​，见图6-14。\n",
    "\n",
    "<img src=\"./images/6.14.png\" alt=\"6.14\" style=\"zoom:50%;\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这种方案看起来复杂，其实写起来很简单，只要把方案8的代码加一行server_.setThreadNum(numThreads)；就行，这里就不举例了。\n",
    "\n",
    "一个程序到底是使用一个event loop还是使用多个event loops呢？[*ZeroMQ的手册给出的建议是，按照每千兆比特每秒的吞吐量配一个event loop的比例来设置event loop的数目*](http://wiki.zeromq.org/area:faq#toc3)，即`muduo::TcpServer::setThreadNum()`的参数。依据这条经验规则，在编写运行于千兆以太网上的网络程序时，用一个event loop就足以应付网络IO。如果程序本身没有多少计算量，而主要瓶颈在网络带宽，那么可以按这条规则来办，只用一个event loop。另一方面，如果程序的IO带宽较小，计算量较大，而且对延迟不敏感，那么可以把计算放到thread pool中，也可以只用一个event loop。\n",
    "\n",
    "值得指出的是，以上假定了TCP连接是同质的，没有优先级之分，我们看重的是服务程序的总吞吐量。但是如果TCP连接有优先级之分，那么单个event loop可能不适合，正确的做法是把高优先级的连接用单独的event loop来处理。\n",
    "\n",
    "在muduo中，属于同一个event loop的连接之间没有事件优先级的差别。我这么设计的原因是为了防止优先级反转。比方说一个服务程序有10个心跳连接，有10个数据请求连接，都归属同一个event loop，我们认为心跳连接有较高的优先级，心跳连接上的事件应该优先处理。但是由于事件循环的特性，如果数据请求连接上的数据先于心跳连接到达（早到1ms）​，那么这个event loop就会调用相应的event handler去处理数据请求，而在下一次epoll_wait()的时候再来处理心跳事件。因此在同一个event loop中区分连接的优先级并不能达到预想的效果。我们应该用单独的event loop来管理心跳连接，这样就能避免数据连接上的事件阻塞了心跳事件，因为它们分属不同的线程。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 结语"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我在§ 3.3曾写道：总结起来，我推荐的C++多线程服务端编程模式为：one loop per thread + thread pool。\n",
    "- event loop用作non-blocking IO和定时器。\n",
    "- thread pool用来做计算，具体可以是任务队列或生产者消费者队列。\n",
    "\n",
    "当时（2010年2月）写这篇博客时我还说：“以这种方式写服务器程序，需要一个优质的基于Reactor模式的网络库来支撑，我只用过in-house的产品，无从比较并推荐市面上常见的C++网络库，抱歉。”\n",
    "现在有了muduo网络库，我终于能够用具体的代码示例把自己的思想完整地表达出来了。\n",
    "\n",
    "归纳一下，实用的方案有5种，muduo直接支持后4种，见表\n",
    "\n",
    "> 此表参考了《Characteristics of multithreading models for high-performance IO driven network applications》\n",
    "\n",
    "| 方案 | 名称                         | 接受新连接 | 网络 IO        | 计算任务       |\n",
    "| ---- | ---------------------------- | ---------- | -------------- | -------------- |\n",
    "| 2    | thread-per-connection        | 1个线程    | N 线程         | 在网络线程进行 |\n",
    "| 5    | 单线程 Reactor               | 1个线程    | 在连接线程进行 | 在连接线程进行 |\n",
    "| 8    | Reactor + 线程池             | 1个线程    | 在连接线程进行 | C₂ 线程        |\n",
    "| 9    | one loop per thread          | 1个线程    | C₁ 线程        | 在网络线程进行 |\n",
    "| 11   | one loop per thread + 线程池 | 1个线程    | C₁ 线程        | C₂ 线程        |\n",
    "\n",
    "表中的N表示并发连接数目，C₁和C₂是与连接数无关、与CPU数目有关的常数。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我再用银行柜台办理业务为比喻，简述各种模型的特点。\n",
    "- 银行有旋转门，办理业务的客户人员从旋转门进出(IO)；银行也有柜台，客户在柜台办理业务（计算）。\n",
    "- 要想办理业务，客户要先通过旋转门进入银行；办理完之后，客户要再次通过旋转门离开银行。\n",
    "- 一个客户可以办理多次业务，每次都必须从旋转门进出（TCP长连接）。\n",
    "- 另外，旋转门一次只允许一个客户通过（无论进出），因为`read()/write()`只能同时调用其中一个。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "方案5：\n",
    "- 这间小银行有一个旋转门、一个柜台，每次只允许一名客户办理业务。而且当有人在办理业务时，旋转门是锁住的（计算和IO在同一线程）。为了维持工作效率，银行要求客户应该尽快办理业务，最好不要在取款的时候打电话去问家里人密码，也不要在通过旋转门的时候停下来系鞋带，这都会阻塞其他堵在门外的客户。如果客户很少，这是很经济且高效的方案；但是如果场地较大（多核），则这种布局就浪费了不少资源，只能并发(concurrent)不能并行(parallel)。如果确实一次办不完，应该离开柜台，到门外等着，等银行通知再来继续办理（分阶段回调）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "方案8：\n",
    "- 这间银行有一个旋转门，一个或多个柜台。银行进门之后有一个队列，客户在这里排队到柜台（线程池）办理业务。即在单线程Reactor后面接了一个线程池用于计算，可以利用多核。旋转门基本是不锁的，随时都可以进出。但是排队会消耗一点时间，相比之下，方案5中客户一进门就能立刻办理业务。另外一种做法是线程池里的每个线程有自己的任务队列，而不是整个线程池共用一个任务队列。这样的好处是避免全局队列的锁争用，坏处是计算资源有可能分配不平均，降低并行度。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "方案9：\n",
    "- 这间大银行相当于包含方案5中的多家小银行，每个客户进大门的时候就被固定分配到某一间小银行中，他的业务只能由这间小银行办理，他每次都要进出小银行的旋转门。但总体来看，大银行可以同时服务多个客户。这时同样要求办理业务时不能空等（阻塞），否则会影响分到同一间小银行的其他客户。而且必要的时候可以为VIP客户单独开一间或几间小银行，优先办理VIP业务。这跟方案5不同，当普通客户在办理业务的时候，VIP客户也只能在门外等着（见图6-11的右图）。这是一种适应性很强的方案，也是muduo原生的多线程IO模型。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "方案11：\n",
    "- 这间大银行有多个旋转门，多个柜台。旋转门和柜台之间没有一一对应关系，客户进大门的时候就被固定分配到某一旋转门中（奇怪的安排，易于实现线程安全的IO，见§ 4.6），进入旋转门之后，有一个队列，客户在此排队到柜台办理业务。这种方案的资源利用率可能比方案9更高，一个客户不会被同一小银行的其他客户阻塞，但延迟也比方案9略大。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- AI\n",
    "\n",
    "| 模型                                  | 银行比喻                                                     | 优点                                                   | 缺点                                                   | 适用场景                                           |\n",
    "| ------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------------- |\n",
    "| thread-per-connection (方案2)         | - 一个接待员负责旋转门<br>- 每个客户配专属柜员<br>- 柜员处理该客户所有事务 | - 实现简单<br>- 客户响应快速<br>- 业务处理独立         | - 人力成本高<br>- 资源消耗大<br>- 柜员数量限制并发     | - 客户数量少<br>- 每个连接负载重<br>- 资源充足     |\n",
    "| 单线程 Reactor (方案5)                | - 一人身兼接待和柜员<br>- 所有事务排队处理<br>- 办理时暂停接待 | - 资源消耗少<br>- 无需同步互斥<br>- 实现简单           | - 不能并行处理<br>- 一人忙不过来<br>- 容易被单客户阻塞 | - 业务处理快速<br>- 并发要求不高<br>- 单机单核场景 |\n",
    "| Reactor + 线程池 (方案8)              | - 一个专职接待员<br>- 多个共享柜员<br>- 统一任务队列         | - IO与计算分离<br>- 充分利用多核<br>- 资源复用好       | - 任务队列可能成瓶颈<br>- 调度开销<br>- 实现较复杂     | - 计算密集场景<br>- 多核环境<br>- 业务可并行       |\n",
    "| one loop per thread (方案9)           | - 多个小银行并行<br>- 客户固定在一家<br>- 各自独立运作       | - 无竞争无同步<br>- 负载均衡好<br>- CPU亲和性好        | - 小银行间负载可能不均<br>- 资源不共享<br>- 实现较复杂 | - IO密集场景<br>- 多核环境<br>- 连接时间长         |\n",
    "| one loop per thread + 线程池 (方案11) | - 多个固定通道<br>- 共享柜员池<br>- 灵活调度                 | - 最大程度利用资源<br>- IO与计算都并行<br>- 扩展性最好 | - 实现最复杂<br>- 调试难度大<br>- 维护成本高           | - 复杂业务场景<br>- 对性能要求高<br>- 需要灵活扩展 |"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "C++17",
   "language": "C++17",
   "name": "xcpp17"
  },
  "language_info": {
   "codemirror_mode": "text/x-c++src",
   "file_extension": ".cpp",
   "mimetype": "text/x-c++src",
   "name": "C++17",
   "version": "17"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
